package hdapi3;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.codec.binary.Base64;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;


public class HD3 {
	private final static Logger g_logger = Logger
			.getLogger(HD3.class.getName());
	
	
	private String realm;
	private String username;
	private String secret;
	private boolean useLocal;
	private String apiServer;
	private String logServer;
	private String siteId;
	private String mobileSite;
	private String nonMobile;
	private String matchFilter;

	private int connectTimeout;
	private int readTimeout;

	private boolean useProxy;
	private String proxyAddress;
	private int proxyPort;
	private String proxyUsername;
	private String proxyPassword;
	private String localFilesDirectory;
	
	private JsonObject m_detectRequest;
	private JsonObject m_reply;
	private JsonObject m_trees;
	private byte[] m_rawReply;
	private String m_error;
	private Cache m_cache;
	private JsonElement m_specsContent;
	private Object m_lock = new Object();
	public HD3() {
		this.realm = "APIv3";
		this.username = Settings.getUsername();
		this.secret = Settings.getSecret();
		this.useLocal = Settings.isUseLocal();
		this.apiServer = Settings.getApiServer();
		this.logServer = Settings.getLogServer();
		this.siteId = Settings.getSiteId();
		this.nonMobile = Settings.getNonMobile();
		this.mobileSite = Settings.getMobileSite();
		this.matchFilter = Settings.getMatchFilter();

		this.connectTimeout = Settings.getConnectTimeout();
		this.readTimeout = Settings.getReadTimeout();

		this.useProxy = Settings.isUseProxy();
		this.proxyAddress = Settings.getProxyAddress();
		this.proxyPort = Settings.getProxyPort();
		this.proxyUsername = Settings.getProxyUsername();
		this.proxyPassword = Settings.getProxyPassword();
		this.localFilesDirectory = Settings.getLocalFilesDirectory();
		
		this.m_cache = new Cache();
	}

	public void setup() {
		setup(null, null, null);
	}
	public void setup(Map<String, String> headers, String serverIpAddress,
			String requestURI) {
		try {
			resetDetectRequest();
			if (headers != null) {
				Set<String> keys = headers.keySet();
				for (String key : keys) {
					if (!"Cookie".equalsIgnoreCase(key))
						this.m_detectRequest.addProperty(key, headers.get(key));
				}
				
			}
			m_detectRequest.addProperty("ipaddress", serverIpAddress);
			m_detectRequest.addProperty("request_uri", requestURI);
		} catch (Exception ex) {
			g_logger.severe(ex.getMessage());
			setError("Failed to setup detect request. Cause: "
					+ ex.getMessage());
		}
	}
	
	public boolean deviceVendors() {
		initRequest();
		return isUseLocal() ? localDeviceVendors() :remote("device/vendors", null);
	}
	
	private boolean localDeviceVendors() {
		JsonObject data = this.localGetSpecs();
		boolean ret = false;
		if (data == null)
			return ret;
		JsonObject reply = new JsonObject();
		List<String> vendors = new ArrayList<String>();
		JsonElement devices = data.get(JsonContants.DEVICES);
		if (devices != null && devices.isJsonArray()) {
			JsonArray arrayTemp = (JsonArray) devices;
			Iterator<JsonElement>  iter = arrayTemp.iterator();
			while (iter.hasNext()) {
				JsonElement temp = iter.next();
				if (temp.isJsonObject()) {
					JsonObject row = (JsonObject) temp;
					JsonObject device = row.getAsJsonObject(JsonContants.DEVICE);
					if (HD3Util.isNullElement(device)) continue;
					JsonObject hdSpecs = device.getAsJsonObject(JsonContants.HD_SPECS);
					if (HD3Util.isNullElement(hdSpecs)) continue;
					JsonElement vendor = hdSpecs.get(JsonContants.GENERAL_VENDOR);
					if (HD3Util.isNullElement(vendor)) continue;
					vendors.add(vendor.getAsString());
				}
			}
			reply.add("vendor", HD3Util.toUniqueJsonArray(vendors));
			reply.addProperty(JsonContants.STATUS, 0);
			reply.addProperty(JsonContants.MESSAGE, "OK");
			this.m_reply = reply;
			ret = true;
		} else {
			m_reply = this.createErrorReply(299, "Error: No devices data");
			setError("Error 299. Message: No devices data");
			g_logger.warning("No data can be found from (devices) node from local data");
		}
		
		return ret;				
	}

	public boolean deviceModels(String vendor) {
		initRequest();
		return isUseLocal()? localDeviceModels(vendor) : remote("device/models/" + vendor, null); 
	}
	
	private boolean localDeviceModels(String vendor) {
		boolean ret = false;
		JsonObject data = this.localGetSpecs();
		if (data == null)
			return ret;
		
		JsonObject reply = new JsonObject();
		List<String> models = new ArrayList<String>();
		JsonElement devices = data.get(JsonContants.DEVICES);
		if (devices != null && devices.isJsonArray()) {
			JsonArray arrayTemp = (JsonArray) devices;
			Iterator<JsonElement>  iter = arrayTemp.iterator();
			while (iter.hasNext()) {
				JsonElement temp = iter.next();
				if (temp.isJsonObject()) {
					JsonObject row = (JsonObject) temp;
					JsonObject device = row.getAsJsonObject(JsonContants.DEVICE);
					if (HD3Util.isNullElement(device)) continue;
					JsonObject hdSpecs = device.getAsJsonObject(JsonContants.HD_SPECS);
					if (HD3Util.isNullElement(hdSpecs)) continue;
					JsonElement vendorElement = hdSpecs.get(JsonContants.GENERAL_VENDOR);
					if (HD3Util.isNullElement(vendorElement)) continue;
					String strVendor = vendorElement.getAsString();
					if (strVendor.equalsIgnoreCase(vendor)) {
						models.add(hdSpecs.get(JsonContants.GENERAL_MODEL).getAsString());
						
					}
					String vendorKey = strVendor + " ";
					JsonElement aliasesElement = hdSpecs.get(JsonContants.GENERAL_ALIASES);
					if (HD3Util.isNullElement(aliasesElement)) continue;
					if (aliasesElement.isJsonArray()) {
						JsonArray tempAliasesArray = (JsonArray)aliasesElement;
						Iterator<JsonElement> aliasesIter = tempAliasesArray.iterator();
						while (aliasesIter.hasNext()) {
							JsonElement alias = aliasesIter.next();
							if (alias.isJsonPrimitive()) {
								String strAlias = alias.getAsString();
								if (!HD3Util.isNullOrEmpty(strAlias) && strAlias.indexOf(vendorKey) == 0) {
									models.add(strAlias.replace(vendorKey, ""));
								}
							}
						}
					} else if (aliasesElement.isJsonPrimitive()) {
						String strAlias = aliasesElement.getAsString();
						if (!HD3Util.isNullOrEmpty(strAlias) && strAlias.indexOf(vendorKey) == 0) {
							models.add(strAlias.replace(vendorKey, ""));
						}
					}
				}
			}
			
			reply.add("model", HD3Util.toUniqueJsonArray(models));
			reply.addProperty(JsonContants.STATUS, 0);
			reply.addProperty(JsonContants.MESSAGE, "OK");
			this.m_reply = reply;
			ret = true;
		} else {
			m_reply = this.createErrorReply(299, "Error: No devices data");
			setError("Error 299. Message: No devices data");
			g_logger.warning("No data can be found from (devices) node from local data");
		}
		
		return ret;
	}
	
	public boolean deviceView(String vendor, String model) {
		initRequest();
		StringBuilder sb = new StringBuilder();
		String service = sb.append("device/view/").append(vendor).append("/").append(model).toString();
		return isUseLocal() ? localDeviceView(vendor, model) : remote(service, null);
	}
	
	private boolean localDeviceView(String vendor, String model) {
		boolean ret = false;
		JsonObject data = this.localGetSpecs();
		if (data == null)
			return ret;
		JsonObject reply = new JsonObject();
		JsonElement devices = data.get(JsonContants.DEVICES);
		if (devices != null && devices.isJsonArray()) {
			JsonArray arrayTemp = (JsonArray) devices;
			Iterator<JsonElement>  iter = arrayTemp.iterator();
			while (iter.hasNext()) {
				JsonElement temp = iter.next();
				if (temp.isJsonObject()) {
					JsonObject row = (JsonObject) temp;
					JsonObject device = row.getAsJsonObject(JsonContants.DEVICE);
					if (HD3Util.isNullElement(device)) continue;
					JsonObject hdSpecs = device.getAsJsonObject(JsonContants.HD_SPECS);
					if (HD3Util.isNullElement(hdSpecs)) continue;
					JsonElement vendorElement = hdSpecs.get(JsonContants.GENERAL_VENDOR);
					JsonElement modelElement = hdSpecs.get(JsonContants.GENERAL_MODEL);
					if (HD3Util.isNullElement(vendorElement) || HD3Util.isNullElement(modelElement)) continue;
					if (vendorElement.getAsString().equalsIgnoreCase(vendor) 
							&& modelElement.getAsString().equalsIgnoreCase(model)) {
						reply.add("device", hdSpecs);
						reply.addProperty(JsonContants.STATUS, 0);
						reply.addProperty(JsonContants.MESSAGE, "OK");
						this.m_reply = reply;
						ret = true;
						return ret;
					}
				}
			}
			m_reply = this.createErrorReply(301, "Error: Nothing found");
			g_logger.warning("_localDeviceView finds no matching device with vendor ("  + vendor + ") and model (" + model + ")");
			setError("Error 301. Message: Nothing found");
		} else {
			m_reply = this.createErrorReply(299, "Error: No devices data");
			setError("Error 299. Message: No devices data");
			g_logger.warning("No data can be found from (devices) node from local data");
		}
		return ret;
	}
	
	public boolean deviceWhatHas(String key, String value) {
		initRequest();
		StringBuilder sb = new StringBuilder();
		String service = sb.append("device/whathas/").append(key).append("/").append(value).toString();
		return isUseLocal()? localDeviceWhatHas(key, value) : remote(service, null);
	}
	
	private boolean localDeviceWhatHas(String key, String value) {
		boolean ret = false;
		JsonObject data = this.localGetSpecs();
		if (data == null)
			return ret;
		JsonObject reply = new JsonObject();
		JsonArray matches = new JsonArray();
		JsonElement devices = data.get(JsonContants.DEVICES);
		if (!HD3Util.isNullElement(devices) && devices.isJsonArray()) {
			ret = true;
			JsonArray arrayTemp = (JsonArray) devices;
			Iterator<JsonElement>  iter = arrayTemp.iterator();
			while (iter.hasNext()) {
				JsonElement temp = iter.next();
				if (temp.isJsonObject()) {
					JsonObject row = (JsonObject) temp;
					JsonObject device = row.getAsJsonObject(JsonContants.DEVICE);
					if (HD3Util.isNullElement(device)) continue;
					JsonObject hdSpecs = device.getAsJsonObject(JsonContants.HD_SPECS);
					if (HD3Util.isNullElement(hdSpecs)) continue;
					JsonElement keyElement = hdSpecs.get(key);
					if (HD3Util.isNullElement(keyElement)) continue;
					boolean match = false;
					//just search entire string no matter if it's array of string
					match = keyElement.toString().toLowerCase().indexOf(value.toLowerCase()) >= 0;

					if (match) {
						JsonObject matched = new JsonObject();
						matched.add("id", device.get(JsonContants.ID));
						JsonElement vendorElement = hdSpecs.get(JsonContants.GENERAL_VENDOR);
						JsonElement modelElement = hdSpecs.get(JsonContants.GENERAL_MODEL);
						matched.add(JsonContants.GENERAL_VENDOR, vendorElement);
						matched.add(JsonContants.GENERAL_MODEL, modelElement);
						matches.add(matched);
						
					}
				}
			}
			reply.add("devices", matches);
			reply.addProperty(JsonContants.STATUS, 0);
			reply.addProperty(JsonContants.MESSAGE, "OK");
			m_reply = reply;
			
		} else {
			m_reply = this.createErrorReply(299, "Error: No devices data");
			setError("Error 299. Message: No devices data");
			g_logger.warning("No data can be found from (devices) node from local data");
		}
		return ret;
	}
	
	
	public boolean siteAdd(JsonObject data) {
		initRequest();
		return remote("site/add", data);
	}
	
	public boolean siteEdit(JsonObject data) {
		initRequest();
		return remote("site/edit/" + getSiteId(), data);
	}
	
	public boolean siteView() {
		initRequest();
		return remote("site/view/" + getSiteId(), null);
	}
	
	public boolean siteDelete() {
		initRequest();
		return remote("site/delete/" + getSiteId(), null);
	}
	
	public boolean siteFetchTrees() {
		initRequest();
		boolean status = remote("site/fetchtrees/" + getSiteId(), null);
		if (!status) return false;
		if (!validateReply()) return false;
			
		try {
			status = HD3Util.saveBytesAsFile(new File(localFilesDirectory, JsonContants.LOCAL_TREES_FILENAME), this.m_rawReply);
		} catch (Exception e) {
			this.createErrorReply(299, "Failed to save trees file.");
			setError("Error 299. Message: failed to save fetched trees due to " + e.getMessage());
			g_logger.severe("Failed to save trees. Cause : " + e.getMessage());
			return false;
		}
		return setCacheTrees();
	}
	
	public boolean siteFetchSpecs() {
		initRequest();
		boolean status = remote("site/fetchspecs/" + getSiteId(), null);
		if (!status) return false;
		if (!validateReply()) return false;
		try {
			status = HD3Util.saveBytesAsFile(new File(localFilesDirectory, JsonContants.LOCAL_SPECS_FILENAME), this.m_rawReply);
		} catch (Exception e) {
			this.createErrorReply(299, "Failed to save specs file.");
			setError("Error 299. Message: failed to save fetched trees due to " + e.getMessage());
			g_logger.severe("Failed to save trees. Cause : " + e.getMessage());
			return false;
		}
		try {
			setCacheSpecs((String)null, (String)null);
		}catch (Exception e) {
			g_logger.warning(e.getMessage());
			return false;
		}
		return true;
	}
	
	private boolean validateReply() {
		if (m_reply == null) {
			setError("Error: No reply");
			return false;
		} else if(HD3Util.isNullElement(m_reply.get(JsonContants.STATUS)) 
					|| !"0".equals(m_reply.get(JsonContants.STATUS).getAsString())) {
			setError("Error: " + m_reply.toString());
			return false;
		}
		return true;
	}
	
	private boolean setCacheTrees() {
		File f = new File(localFilesDirectory, JsonContants.LOCAL_TREES_FILENAME);
		if (f.exists()) {
			FileInputStream fis = null;
			try {
				fis = new FileInputStream(f);
				JsonElement treesContent = HD3Util.parseJson(fis);
				if (!HD3Util.isNullElement(treesContent) && treesContent.isJsonObject()) {
					JsonObject root = (JsonObject) treesContent;
					JsonObject trees = root.get(JsonContants.TREES).getAsJsonObject();
					this.m_trees = trees;
					Set<Map.Entry<String, JsonElement>>  set = trees.entrySet();
					Iterator<Map.Entry<String, JsonElement>> iter= set.iterator();
					while (iter.hasNext()) {
						Map.Entry<String, JsonElement> entry = iter.next();
						this.m_cache.put(entry.getKey(), entry.getValue());
					}
					return true;
				}
			} catch (Exception e) {
				try {
					if (fis != null) fis.close();
				} catch (Exception e2) {
					g_logger.warning(e2.getMessage());
				}
			}
		} 
		this.createErrorReply(299, "Unable to open specs file hd3trees.json");
		setError("Error : 299, Message : setCacheTrees cannot open hd3trees.json. Is it there ? Is it world readable ?");
		return false;

	}

	
	public void addDetectVar(String key, String value) {
		if (this.m_detectRequest == null) {
			this.m_detectRequest = new JsonObject();
			
		}
		this.m_detectRequest.addProperty(key, value);
	}
	public boolean siteDetect() {
		initRequest();
		String id = getSiteId();
		if (HD3Util.isNullElement(m_detectRequest.get(JsonContants.USER_AGENT))
				|| !validateUserAgent(m_detectRequest.get(JsonContants.USER_AGENT).getAsString())) {
			this.createErrorReply(301, "FastFail : Probable bot, spider or script");
		} 

		if (isUseLocal()) {
			g_logger.fine("starting local detecting");
			boolean result = localSiteDetect();
			return result;
		} else {
			return remote("site/detect/" + id, this.m_detectRequest) 
				&& !HD3Util.isNullElement(m_reply.get(JsonContants.STATUS))
				&& m_reply.get(JsonContants.STATUS).getAsString().equals("0");
		}
	}
	
	private boolean localSiteDetect() {
		JsonObject device = null;

		JsonElement id = getDevice();
		if (!HD3Util.isNullElement(id)) {
			device = getCacheSpecs(id.getAsString(), JsonContants.DEVICE.toLowerCase());
			if (device == null) {
				createErrorReply(225, "Unable to write cache or main datafile.");
				setError("Unable to write cache or main datafile.");
				return false;
			}
			JsonElement platformId = getExtra(JsonContants.PLATFORM);
			JsonElement browserId = getExtra(JsonContants.BROWSER);
			JsonObject platform = null;
			JsonElement browser = null;
			if (!HD3Util.isNullElement(platformId)) {
				platform = (JsonObject) getCacheSpecs(platformId.getAsString(), JsonContants.EXTRA.toLowerCase());
			} 
			if (platform == null) {
				platform = new JsonObject();
			}
			if (!HD3Util.isNullElement(browserId)) {
				browser = getCacheSpecs(browserId.getAsString(), JsonContants.EXTRA.toLowerCase());
				JsonElement generalBrowser = null;
				JsonElement generalBrowserVersion = null;
				if (!HD3Util.isNullElement(browser)) {
					JsonObject browserObj = browser.getAsJsonObject();
					generalBrowser = browserObj.get(JsonContants.GENERAL_BROWSER);
					generalBrowserVersion = browserObj.get(JsonContants.GENERAL_BROWSER_VERSION);
				}
				if (!HD3Util.isNullElement(generalBrowser)) {
					platform.add(JsonContants.GENERAL_BROWSER, generalBrowser);
					platform.add(JsonContants.GENERAL_BROWSER_VERSION, generalBrowserVersion);
				}
				JsonElement generalPlatform = platform.get(JsonContants.GENERAL_PLATFORM);
				JsonElement generalPlatformVersion  = platform.get(JsonContants.GENERAL_PLATFORM_VERSION);
				device.add(JsonContants.GENERAL_PLATFORM, generalPlatform);
				device.add(JsonContants.GENERAL_PLATFORM_VERSION, generalPlatformVersion);
				if (!HD3Util.isNullElement(generalBrowser)) {
					device.add(JsonContants.GENERAL_BROWSER, generalBrowser);
					device.add(JsonContants.GENERAL_BROWSER_VERSION, generalBrowserVersion);
				}
			}
			this.m_reply = new JsonObject();
			m_reply.add(JsonContants.HD_SPECS, device);
			m_reply.addProperty(JsonContants.STATUS, 0);
			m_reply.addProperty(JsonContants.MESSAGE, "OK");
			if (HD3Util.isNullElement(device.get(JsonContants.GENERAL_TYPE))) {
				m_reply.addProperty(JsonContants.CLASS_ATTR, "Unknown");
			} else {
				m_reply.addProperty(JsonContants.CLASS_ATTR, device.get(JsonContants.GENERAL_TYPE).getAsString());
			}
			return true;
		}
		
		return false;
	}
	
	private JsonObject getCacheSpecs(String id, String type) {
		if (HD3Util.isNullOrEmpty(id) || HD3Util.isNullOrEmpty(type)) return null;
		Object temp  = (Object)m_cache.get(type.toLowerCase()+id);
		if (temp instanceof JsonObject) {
			JsonObject ret = (JsonObject) temp;
			return ret;
		} else {
			return setCacheSpecs(id, type);
		}
			}
	
	private JsonObject setCacheSpecs(String id, String type) {
		File f = new File(localFilesDirectory, JsonContants.LOCAL_SPECS_FILENAME);
		JsonObject returnedSpecs = null;
		if (f.exists()) {
			FileInputStream fis = null;
			try {
				if (m_specsContent == null) {
					synchronized (m_lock) {
						fis = new FileInputStream(f);
						m_specsContent = HD3Util.parseJson(fis);
					}
				}
				if (!HD3Util.isNullElement(m_specsContent) && m_specsContent.isJsonObject()) {
					JsonObject root = (JsonObject) m_specsContent;
					JsonElement devices = root.get(JsonContants.DEVICES);
					if (!HD3Util.isNullElement(devices) && devices.isJsonArray()) {
						JsonArray arrayTemp = (JsonArray) devices;
						Iterator<JsonElement>  iter = arrayTemp.iterator();
						while (iter.hasNext()) {
							JsonElement temp = iter.next();
							if (temp.isJsonObject()) {
								JsonObject row = (JsonObject) temp;
								JsonObject device = row.getAsJsonObject(JsonContants.DEVICE);
								if (HD3Util.isNullElement(device)) continue;
								String deviceId = device.get(JsonContants.ID).getAsString();
								JsonObject hdSpecs = device.getAsJsonObject(JsonContants.HD_SPECS);
								if (HD3Util.isNullElement(hdSpecs)) continue;
								StringBuilder sb = new StringBuilder();
								sb.append(JsonContants.DEVICE.toLowerCase());
								sb.append(deviceId);
								m_cache.put(sb.toString(), hdSpecs);
								if (!HD3Util.isNullOrEmpty(id)&& id.equals(deviceId) && JsonContants.DEVICE.equalsIgnoreCase(type)) {
									returnedSpecs = hdSpecs;
								}
							}
						}
					}
					JsonElement extras = root.get(JsonContants.EXTRAS);
					if (!HD3Util.isNullElement(extras) && extras.isJsonArray()) {
						JsonArray arrayTemp = (JsonArray) extras;
						Iterator<JsonElement>  iter = arrayTemp.iterator();
						while (iter.hasNext()) {
							JsonElement temp = iter.next();
							if (temp.isJsonObject()) {
								JsonObject row = (JsonObject) temp;
								JsonObject extra = row.getAsJsonObject(JsonContants.EXTRA);
								if (HD3Util.isNullElement(extra)) continue;
								String extraId = extra.get(JsonContants.ID).getAsString();
								JsonObject extraSpecs = extra.getAsJsonObject(JsonContants.HD_SPECS);
								if (HD3Util.isNullElement(extraSpecs)) continue;
								m_cache.put(JsonContants.EXTRA.toLowerCase()+extraId, extraSpecs);
								if (!HD3Util.isNullOrEmpty(id)&& id.equals(extraId) && JsonContants.EXTRA.equalsIgnoreCase(type)) {
									returnedSpecs = extraSpecs;
								}
							}
						}
					}
				}
			} catch (Exception e) {
				try {
					if (fis != null) fis.close();
				} catch (Exception e2) {
					g_logger.warning(e2.getMessage());
				}
			}

		} else {
			this.createErrorReply(299, "Unable to open specs file hd3specs.json");
			setError("Error : 299, Message : setCacheTrees cannot open hd3specs.json. Is it there ? Is it world readable ?");
		}
		return returnedSpecs;
	}
	
	private JsonElement getDevice() {
		String agent = null;
		HashMap<String, String> headers = HD3Util.parseHeaders(m_detectRequest);
		if (!HD3Util.isNullOrEmpty(headers.get(JsonContants.X_OPERAMINI_PHONE))
				&&!"? # ?".equals(headers.get(JsonContants.X_OPERAMINI_PHONE))) {
			JsonElement id = matchDevice(JsonContants.X_OPERAMINI_PHONE, headers.get(JsonContants.X_OPERAMINI_PHONE));
			if (!HD3Util.isNullElement(id)) {
				return id;
			}
			headers.remove(JsonContants.X_OPERAMINI_PHONE);
		}
		
		if (!HD3Util.isNullOrEmpty(headers.get(JsonContants.PROFILE))) {
			JsonElement id = matchDevice(JsonContants.PROFILE, headers.get(JsonContants.PROFILE));
			if (!HD3Util.isNullElement(id)) {
				return id;
			}
			headers.remove(JsonContants.PROFILE);				
		}
		
		if (!HD3Util.isNullOrEmpty(headers.get(JsonContants.X_WAP_PROFILE))) {
			JsonElement id = matchDevice(JsonContants.X_WAP_PROFILE, headers.get(JsonContants.X_WAP_PROFILE));
			if (!HD3Util.isNullElement(id)) {
				return id;
			}
			headers.remove(JsonContants.X_WAP_PROFILE);				
		}
		ArrayList<String> order = new ArrayList<String>();
		order.add("x-operamini-phone-ua");
		order.add("x-mobile-ua");
		order.add(JsonContants.USER_AGENT);
		Set<String> keys = headers.keySet();
		Iterator<String> iter = keys.iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			if (!order.contains(key)) {
				Pattern p = Pattern.compile("/^x-/i");
				Matcher m = p.matcher(key);
				if (m.matches()) {
					order.add(key);
				}
			}
		}
		agent = headers.get(JsonContants.USER_AGENT);
		for (int i = 0 ; i < order.size() ; i++) {
			String header = order.get(i);
			if (!HD3Util.isNullOrEmpty(headers.get(header))) {
				JsonElement id = matchDevice(JsonContants.USER_AGENT, headers.get(header));
				if (!HD3Util.isNullElement(id)) return id;
			}
		}
		return matchDevice(JsonContants.USER_AGENT, agent, 1);
		
	}
	
	private JsonElement matchDevice(String header, String value) {
		return matchDevice(header, value, 0);
	}
	
	private JsonElement matchDevice(String header, String value, int generic) {
		if (HD3Util.isNullOrEmpty(value)) return null;
		value = value.toLowerCase().replaceAll(getMatchFilter(), "");
		String treeTag = header + generic;
		return match(header, value, treeTag);
	}
	
	private JsonElement match(String header, String newValue, String treeTag) {

		if (HD3Util.isNullOrEmpty(newValue) || newValue.length() < 4) {
			return null;
		}
		JsonElement branch = getBranch(treeTag);
		if (HD3Util.isNullElement(branch)) return null;
		if (JsonContants.USER_AGENT.equals(header)) {
			if (branch.isJsonObject()) {
				JsonObject branchObj = (JsonObject) branch;
				Set<Map.Entry<String, JsonElement>> branchEntries = branchObj.entrySet();
				Iterator<Map.Entry<String, JsonElement>> branchIter = branchEntries.iterator();
				while (branchIter.hasNext()) {
					Map.Entry<String, JsonElement> branchItem = branchIter.next();
					String order = branchItem.getKey();
					//g_logger.fine("order : " + order);
					JsonElement filterElem = branchItem.getValue(); 
					if (filterElem instanceof JsonObject) {
						JsonObject filterObj = (JsonObject) filterElem;
						Set<Map.Entry<String, JsonElement>> filterEntries = filterObj.entrySet();
						Iterator<Map.Entry<String, JsonElement>> filterIter = filterEntries.iterator();
						while (filterIter.hasNext()) {
							Map.Entry<String, JsonElement> filterItem = filterIter.next();
							String filter = filterItem.getKey();
							//g_logger.fine("filter : " + filter);
							if (newValue.indexOf(filter) >= 0) {
								JsonElement matchesElem = filterItem.getValue();
								if (matchesElem instanceof JsonObject) {
									JsonObject matchesObj = (JsonObject) matchesElem;
									Set<Map.Entry<String, JsonElement>> matchesEntries = matchesObj.entrySet();
									Iterator<Map.Entry<String, JsonElement>> matchesIter = matchesEntries.iterator();
									while (matchesIter.hasNext()) {
										Map.Entry<String, JsonElement> matchItem = matchesIter.next();
										String match = matchItem.getKey();
										//g_logger.fine("match : " + match);
										JsonElement nodeElem = matchItem.getValue();
										if (newValue.indexOf(match) >= 0) {
											//g_logger.fine("matched : " + newValue);
											return nodeElem;
										}
									}
								}
							}
						}
						
					}
					//if ()
				}
			}
		} else {
			if (branch.isJsonObject()) {
				JsonObject temp = (JsonObject) branch;
				return temp.get(newValue);
			}
		}
		return null;
	}
	private JsonElement getBranch(String name) {
		if (HD3Util.isNullOrEmpty(name)) return null;
		if (m_trees != null) {
			g_logger.fine(name + "fetched from memory");
			return m_trees.get(name);
		}
		if (m_cache.get(name) != null && m_cache.get(name) instanceof JsonElement) {
			return (JsonElement)m_cache.get(name);
		}
		
		this.setCacheTrees();
		if (m_trees.get(name) == null) {
			m_trees.add(name, new JsonArray());
			g_logger.fine(name + " built and cached");
		}
		return m_trees.get(name);
		/*
		 * if (! empty($this->tree[$branch])) {
			$this->_hd3log("$branch fetched from memory");
			return $this->tree[$branch];
		}

		$tmp = Cache::read($branch);
		if ($tmp !== false) {
			$this->_hd3log("$branch fetched from cache");
			$this->tree[$branch] = $tmp;
			return $tmp;
		}			

		$this->_setCacheTrees();
		if (empty($this->tree[$branch]))
			$this->tree[$branch] = array();

		$this->_hd3log("$branch built and cached");
		return $this->tree[$branch];
		 */
	}
	
	private JsonElement getExtra(String classKey) {
		HashMap<String, String> headers = HD3Util.parseHeaders(m_detectRequest);
		ArrayList<String> checkOrder = new ArrayList<String>();
		if (JsonContants.PLATFORM.equals(classKey)) {
			checkOrder.add("x-operamini-phone-ua");
			checkOrder.add(JsonContants.USER_AGENT);
			checkOrder.addAll(headers.keySet());
		} else if (JsonContants.BROWSER.equals(classKey)) {
			checkOrder.add("agent");
			checkOrder.addAll(headers.keySet());
		}
		for (String field : checkOrder) {
			if (!HD3Util.isNullOrEmpty(headers.get(field)) 
					&& (JsonContants.USER_AGENT.equals(field) || field.indexOf("x-") >=0)){
				JsonElement id = matchExtra(JsonContants.USER_AGENT, headers.get(field),classKey);
				return id;
			}
		}
		return null;

	}
	
	private JsonElement matchExtra(String header ,String value, String classKey) {
		value = value.toLowerCase().replaceAll(" ", "");
		String treeTag = header + classKey;
		return match(header, value, treeTag);
		
	}
	
	private boolean validateUserAgent(String userAgent) {
		if (HD3Util.isNullOrEmpty(userAgent)) return false;
		Pattern p = Pattern.compile(getNonMobile());
		Matcher m = p.matcher(userAgent);
		if (m.matches()) {
			return false;
		}
		return true;
	}
	
	private boolean remote(String service, JsonObject data) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ByteArrayInputStream in = null;
		boolean ret = false;
		try {
			if (this.post(data == null ? null : data.toString(), service, out) ) {
				byte[] content = out.toByteArray();
				this.m_rawReply = content;
				in = new ByteArrayInputStream(content);
				JsonElement response = HD3Util.parseJson(in);
				if (response == null || response.isJsonNull()) {
					this.setError("Empty reply");
				} else if (response.isJsonObject()) {
					JsonObject temp = (JsonObject) response;
					this.m_reply = temp;
					if (temp.get(JsonContants.STATUS) == null) {
						this.setError("Error : No status set in reply");
					} else if (temp.get("status").getAsInt() != 0) {
						int status = temp.get(JsonContants.STATUS).getAsInt();
						String message = temp.get(JsonContants.MESSAGE) == null ? "": temp.get(JsonContants.MESSAGE).getAsString();
						StringBuilder sb = new StringBuilder();
						sb.append("Error : ").append(status).append(", Message : ").append(message);
						this.setError(sb.toString());
					} else {
						ret = true;
					}
				} else {
					this.setError("Error: No JsonObject in response.");
				}
			}
		} finally {
			try {
				out.close();
			} catch (Exception e) {
				g_logger.warning(e.getMessage());
			}
			try {
				if (in != null) {
					in.close();
				}
			} catch (Exception e) {
				g_logger.warning(e.getMessage());
			}
		}
		return ret;
	}

	private boolean post(String data, String service,OutputStream response) {
		boolean ret = false;
		try {
			String apiUrl = getApiServer();
			String contentLength = "0";
			if (!HD3Util.isNullOrEmpty(data)) {
				contentLength = Integer.toString(data.length());
			}
			URL oldURL = new URL(apiUrl.toLowerCase());
			InetAddress[] addresses = new InetAddress[0];
			addresses = resolveHostAddress(oldURL);
			
			ArrayList<Integer> randomIndexes = createRandomIndexArray(addresses.length);
			for (int i = 0; i < addresses.length; i++) {
				try {
					InetAddress address = addresses[randomIndexes.get(i)];
					StringBuilder sb = new StringBuilder();
					sb.append("http://").append(address.getHostAddress()).append("/").append(getRealm().toLowerCase()).append("/").append(service).append(".json");
					URL newURL = new URL(sb.toString());
					URLConnection conn;
					if (isUseProxy()
							&& !HD3Util.isNullOrEmpty(getProxyAddress())) {
						Proxy proxy = new Proxy(Proxy.Type.HTTP,
								new InetSocketAddress(getProxyAddress(),
										getProxyPort()));
						conn = newURL.openConnection(proxy);
						if (!HD3Util.isNullOrEmpty(getProxyUsername())) {
							conn.setRequestProperty("Proxy-Authorization",
									getBasicProxyPass());
						}
					} else {
						g_logger.fine("connecting to : " + newURL.toExternalForm());
						conn = newURL.openConnection();
					}
					conn.setConnectTimeout(getConnectTimeout() * 1000);
					// = url.openConnection();
					conn.setDoOutput(true);
					conn.setRequestProperty("Content-Type", "application/json");
					conn.setRequestProperty("Content-Length",contentLength);
					conn.setRequestProperty("Authorization",
							getAuthorizationHeader(newURL));

					conn.setReadTimeout(getReadTimeout() * 1000);

					OutputStreamWriter writer = new OutputStreamWriter(
							conn.getOutputStream());
					if (!HD3Util.isNullOrEmpty(data)) {
						writer.write(data);
					}
					writer.flush();
					writer.close();
					InputStream is = conn.getInputStream();
					byte b = -1;
					while ((b = (byte) is.read()) != -1) {
						response.write(b);
					}
					response.flush();
					is.close();

					return ret = true;
				} catch (Exception ex) {
					g_logger.warning("Exception occured while trying to access "
							+ addresses[randomIndexes.get(i)]
							+ ex.getLocalizedMessage());
				}
			}
		} catch (Exception e) {
			g_logger.severe(e.getMessage());
			ret = false;
		}
		return ret;
	}

	private String getAuthorizationHeader(URL requestUrl)
			throws Exception {
		String nc = "00000001";
		String snonce = "APIv3";
		StringBuilder sb = new StringBuilder();
		sb.append(System.currentTimeMillis()).append(getSecret());
		String cnonce = HD3Util.md5(sb.toString());
		String qop = "auth";
		sb.setLength(0);
		sb.append(getUsername()).append(":").append(this.getRealm())
				.append(":").append(getSecret());
		String ha1 = HD3Util.md5(sb.toString());
		sb.setLength(0);
		sb.append("POST:").append(requestUrl.getPath());
		String ha2 = new String(HD3Util.md5(sb.toString()));
		sb.setLength(0);
		sb.append(ha1).append(":").append(snonce).append(":").append(nc)
				.append(":").append(cnonce).append(":").append(qop).append(":")
				.append(ha2);
		String response = HD3Util.md5(sb.toString());
		sb.setLength(0);
		sb.append("Digest username=\"").append(getUsername())
				.append("\", realm=\"").append(getRealm()).append("\", nonce=\"")
				.append(snonce).append("\", uri=\"")
				.append(requestUrl.getPath()).append("\", qop=").append(qop)
				.append(", nc=").append(nc).append(", cnonce=\"")
				.append(cnonce).append("\", response=\"").append(response)
				.append("\", opaque=\"").append(getRealm()).append("\"");
		return sb.toString();
	}

	private InetAddress[] resolveHostAddress(URL url)
			throws UnknownHostException {
		String host = url.getHost();
		InetAddress[] ret = InetAddress.getAllByName(host);
		return ret;
	}

	private ArrayList<Integer> createRandomIndexArray(int length) {
		ArrayList<Integer> orderedArray = new ArrayList<Integer>();
		ArrayList<Integer> randomArray = new ArrayList<Integer>();
		for (int i = 0; i < length; i++) {
			orderedArray.add(i);
		}
		for (int i = 0; i < length; i++) {
			int index = (int) (Math.random() * 1000) % length;
			while (randomArray.contains(index)) {
				index = (int) (Math.random() * 1000) % length;
			}
			randomArray.add(index);
		}
		return randomArray;
	}

	private String getBasicProxyPass() {
		StringBuilder sb = new StringBuilder();
		sb.append("Basic ");
		sb.append(new String(Base64.encodeBase64((new String(getProxyUsername()
				+ ":" + getProxyPassword())).getBytes())));
		return sb.toString();

	}
	
	private JsonObject localGetSpecs()  {
		JsonObject ret = null;
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(new File(localFilesDirectory, JsonContants.LOCAL_SPECS_FILENAME));
			JsonParser parser = new JsonParser();
			JsonElement data = parser.parse(new InputStreamReader(fis));
			if (data == null || data.isJsonNull() || !data.isJsonObject()) {
				this.m_reply = createErrorReply(299, "Unable to open specs file hd3specs.json");
				this.setError("Error: 299, Message : _localGetSpecs cannot open hd3specs.json. Is it there ? Is it world readable ?");
			} else {
				ret = (JsonObject) data;
			}
		} catch (FileNotFoundException e) {
			g_logger.severe(e.getMessage());
			this.m_reply = createErrorReply(299, "Unable to open specs file hd3specs.json");
			this.setError("Error: 299, Message : " + e.getMessage());
		}
		
		return ret;
	}
	
	private JsonObject createErrorReply(int status , String msg) {
		JsonObject temp = new JsonObject();
		temp.addProperty(JsonContants.STATUS, status);
		temp.addProperty(JsonContants.MESSAGE, msg);
		return temp;
	}
	
	private void initRequest() {
		m_reply = null;
		m_rawReply = null;
		setError("");
	}

	public String getRealm() {
		return realm;
	}

	public void setRealm(String realm) {
		this.realm = realm;
	}

	public String getError() {
		return m_error;
	}

	public void setError(String error) {
		this.m_error = error;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getSecret() {
		return secret;
	}

	public void setSecret(String secret) {
		this.secret = secret;
	}

	public boolean isUseLocal() {
		return useLocal;
	}

	public void setUseLocal(boolean useLocal) {
		this.useLocal = useLocal;
	}

	public String getApiServer() {
		return apiServer;
	}

	public void setApiServer(String apiServer) {
		this.apiServer = apiServer;
	}

	public String getLogServer() {
		return logServer;
	}

	public void setLogServer(String logServer) {
		this.logServer = logServer;
	}

	public String getSiteId() {
		return siteId;
	}

	public void setSiteId(String siteId) {
		this.siteId = siteId;
	}

	public String getMobileSite() {
		return mobileSite;
	}

	public void setMobileSite(String mobileSite) {
		this.mobileSite = mobileSite;
	}

	public String getMatchFilter() {
		return matchFilter;
	}

	public void setMatchFilter(String matchFilter) {
		this.matchFilter = matchFilter;
	}

	public int getConnectTimeout() {
		return connectTimeout;
	}

	public void setConnectTimeout(int connectTimeout) {
		this.connectTimeout = connectTimeout;
	}

	public int getReadTimeout() {
		return readTimeout;
	}

	public void setReadTimeout(int readTimeout) {
		this.readTimeout = readTimeout;
	}

	public boolean isUseProxy() {
		return useProxy;
	}

	public void setUseProxy(boolean useProxy) {
		this.useProxy = useProxy;
	}

	public String getProxyAddress() {
		return proxyAddress;
	}

	public void setProxyAddress(String proxyAddress) {
		this.proxyAddress = proxyAddress;
	}

	public int getProxyPort() {
		return proxyPort;
	}

	public void setProxyPort(int proxyPort) {
		this.proxyPort = proxyPort;
	}

	public String getProxyUsername() {
		return proxyUsername;
	}

	public void setProxyUsername(String proxyUsername) {
		this.proxyUsername = proxyUsername;
	}

	public String getProxyPassword() {
		return proxyPassword;
	}

	public void setProxyPassword(String proxyPassword) {
		this.proxyPassword = proxyPassword;
	}
	
	public JsonObject getDetectRequest() {
		return m_detectRequest;
	}

	public void setDetectRequest(JsonObject request) {
		this.m_detectRequest = request;
	}
	
	public void resetDetectRequest() {
		this.m_detectRequest = new JsonObject();
	}

	public JsonObject getReply() {
		return m_reply;
	}

	
	public byte[] getRawReply() {
		return m_rawReply;
	}

	public String getNonMobile() {
		return nonMobile;
	}

	public void setNonMobile(String nonMobile) {
		this.nonMobile = nonMobile;
	}

	public static void main(String[] args) {
		Logger topLogger = java.util.logging.Logger.getLogger("");
		Handler consoleHandler = null;
		for (Handler handler : topLogger.getHandlers()) {
			if (handler instanceof ConsoleHandler) {
				consoleHandler = handler;
				break;
			}
		}
		if (consoleHandler == null) {
			consoleHandler = new ConsoleHandler();
			topLogger.addHandler(consoleHandler);
		}
		// set the console handler to fine:
		consoleHandler.setLevel(java.util.logging.Level.FINEST);
		g_logger.setLevel(Level.FINEST);
		
		try {
			FileInputStream fis = new FileInputStream("hdapi_config.properties");
			Settings.init(fis);
			fis.close();
			HD3 hd3 = new HD3();
			hd3.setup(null, "127.0.0.1", "http://localhost");
			if (hd3.deviceVendors()) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			if (hd3.deviceModels("Nokia")) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			if (hd3.deviceView("Nokia", "660")) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
		    if (hd3.deviceWhatHas("general_vendor", "Nokia")) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			if (hd3.siteView()) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			hd3.addDetectVar("user-agent", "Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/12.0.013; Profile/MIDP-2.0 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413");
			if (hd3.siteDetect()) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			hd3.addDetectVar("user-agent", "Opera/9.80 (Android; OperaMini/7.0.29952/28.2144; U; pt) Presto/2.8.119 Version/11.10");
			hd3.addDetectVar("x-operamini-phone", "Android #");
			hd3.addDetectVar("x-operamini-phone-ua", "Mozilla/5.0 (Linux; U;Android 2.1-update1; pt-br; U20a Build/2.1.1.A.0.6) AppleWebKit/530.17(KHTML, like Gecko) Version/4.0 Mobile Safari/530.17");
			if (hd3.siteDetect()) {
				g_logger.fine(hd3.getReply().toString());
			} else {
				g_logger.severe(hd3.getError());
			}
			if (hd3.siteFetchTrees()) {
				g_logger.fine("trees fetched.");
			} else {
				g_logger.severe(hd3.getError());
			}
			if (hd3.siteFetchSpecs()) {
				g_logger.fine("specs fetched.");
			} else {
				g_logger.severe(hd3.getError());
			}
		} catch (Exception ie) {
			ie.printStackTrace();
			g_logger.severe(ie.getMessage());
		}
		
	}
}
